浅谈Android自定义Lint规则的实现 (一)

最近在做一个基于Android Lint的自定义静态代码检查功能库,这里做一个简单的总结。前半部分介绍SDK自带Android Lint的功能与配置使用方法,后半部分介绍扩展自定义Lint规则库的开发流程。

关于Lint的一些基本知识,以及自定义Lint如何实现,可以参考我的系列文章:
Android Lint工作原理剖析
浅谈Android自定义Lint规则的实现 (一)
浅谈Android自定义Lint规则的实现 (二)

相关Demo代码可以参见我的github代码库:
CustomLintDemo

什么是Android Lint

Android Lint是一个静态代码分析工具,它能够对你的Android项目中潜在的bug、可优化的代码、安全性、性能、可用性、可访问性、国际化等进行检查。

在Android SDK Tools 16及更高的版本中,Lint工具会自动安装。通过它对Android工程源代码进行扫描和检查,可发现潜在的问题,以便程序员及早修正这个问题。Android Lint提供了命令行方式执行,还与IDE(如Android Studio)进行了集成,并提供了xml和html形式的输出报告。

看了上面的介绍可能大家依然很迷惑“这货到底有啥用”,其实我们平时在Android开发过程中一直在享受Lint带来的便利。比如,下面图中的警告和错误提示,相信大家应该很熟悉吧:

上面的例子分别是java文件与Manifest文件在接受Lint检查后给出的简要lint报告,是Lint与IDE集成后的一种表现形式。事实上,Android Lint目前能检查的项目已经多达220项,检查的范围涵盖了二进制资源文件、java源代码、class文件、gradle配置文件、xml文件、resource文件夹、其他文件等。除了图中这种与IDE结合的简洁报告形式以外,也提供更详细的html和xml形式的报告,让你对自己代码质量的提升空间有更全面的认识。

在Android Studio中,每一次编译程序时都会自动运行lint分析工具,也可以在需要lint分析的文件夹、包或文件上点击右键选择【Analyze】->【Inspect Code】。 生成的报告包含了检查过程中发现的问题,并把这些内容按照类别、优先级、严重程度进行了区分。

Lint工具的处理流程如下图所示:
Lint工作流

图中各部分含义如下:

  • Application source files: 构成你Android project的源文件,包含Java和XML文件,图标,以及ProGuard配置文件。
  • lint.xml: 配置文件,用来指定你想禁用哪些lint检查功能,以及自定义问题严重度(problem severity levels)。
  • lint Tool: 一个可以从命令行或Android Studio中运行的静态打码扫描工具。
  • lint Output: lint检查的结果,可以在命令行中通过lint查看,也可以在Android Studio的Event Log中查看。

Android Lint检查哪些内容

Android Lint内置了很多lint规则,到现在为止是220项检查,总共可以分为以下几类:

  • Correctness 正确性
  • Security 安全性
  • Performance 性能
  • Usability 可用性
  • Accessibility 可访问性
  • Internationalization 国际化

下面列举一些常见的lint会检测的代码问题:

  • 缺少翻译(和未使用的翻译)
  • 布局性能问题(老的layoutopt工具会用于查找所有这样的问题,和除此之外更多的问题)
  • 未使用的资源
  • 不一致的数组大小(当在多个配置中定义数组)
  • 可访问性和国际化问题(硬编码字符串,缺少contentDescription等)
  • 图标问题 (如丢失密度、 重复图标、 错误尺寸等)
  • 可用性问题 (如不在文本字段上指定输入的类型)
  • 清单错误

如果要查看lint工具支持的issue的完整列表和它们所对应的issue ID,可以使用lint --list命令。

配置Android Lint

默认情况下,当你运行Lint扫描时,它会对Lint支持的所有issue进行检查。你也可以限制只让lint检查特定的issue,并为某些issue分配严重度(severity level)。
比如,你可以禁止lint检查那些与你的项目无关的issue,并为lint配置一个更低的severity level来让它报告那些不是非常严重的issue。

你可以为lint检查配置不同的level:

  • 全局(对整个project)
  • 每个project module
  • 每个production module
  • 每个test module
  • 每个open files
  • 每个class hierarchy
  • 每个Version Control System (VCS) scopes

在Android Studio中配置Lint

Android Studio允许你对lint每项检查单独启用或禁用,还可以对项目全局、特定文件夹、特定文件进行专门的lint配置。方法是在Android Studio中点击File > Settings > Project Settings菜单打开Editor->Inspections页面,里面有它支持的Profiles和Inspections列表,如图:
Inspection Configuration

配置Lint文件

你可以在lint.xml文件中指定你对lint检查的偏好设置。如果你要手动创建这个文件,就把它放在你的Android工程的根目录中。如果你是在Android Studio中配置lint偏好,那么lint.xml文件会自动创建并添加到你的Android工程中。

lint.xml文件的组成结构是,最外面是一对闭合的标签,里面包含一个或多个子元素。每一个被唯一的id属性来标识,整体结构如下:

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- list of issues to configure -->
</lint>

通过设置标签中的severity属性值,你可以对某个issue禁用lint检查,或者修改某个issue的严重程度(severity level)。

一个实例lint.xml文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Disable the given check in this project -->
<issue id="IconMissingDensityFolder" severity="ignore" />

<!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
<issue id="ObsoleteLayoutParam">
<ignore path="res/layout/activation.xml" />
<ignore path="res/layout-xlarge/activation.xml" />
</issue>

<!-- Ignore the UselessLeaf issue in the specified file -->
<issue id="UselessLeaf">
<ignore path="res/layout/main.xml" />
</issue>

<!-- Change the severity of hardcoded strings to "error" -->
<issue id="HardcodedText" severity="error" />
</lint>

在Java源文件或XML源文件中配置lint检查

在Java中配置lint检查

要对Android项目中某个Java类或方法禁用lint检查,只需要对那段代码添加@SuppressLint注解即可。
下面的例子显示了如何对onCreate方法关闭NewApi这个issue的lint检查。lint工具仍然会对这个类的其他方法进行NewApi issue的检查。例子如下:

1
2
3
4
5
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

下面的例子显示如何对FeedProvider类关闭ParserError issue的lint检查:

1
2
3
4
@SuppressLint("ParserError")
public class FeedProvider extends ContentProvider {
...
}

如果要在Java文件中禁用所有issue的lint检查,使用all关键字,比如:

1
@SuppressLint("all")

在XML中配置lint检查

如果要对XML文件中某一部分禁用lint检查,可以使用tools:ignore属性来标识。为了让这个属性能够被lint工具识别,必须把下面的命名空间加入你的XML中:

1
namespace xmlns:tools="http://schemas.android.com/tools"

下面的例子显示了如何对XML布局文件中的元素禁用UnusedResources issue的lint检查。ignore属性会被该元素下的子元素继承,在这个例子中,子元素同样被禁用了lint检查。

1
2
3
4
5
6
7
8
<LinearLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="UnusedResources" >


<TextView
android:text="@string/auto_update_prompt" />

</LinearLayout>

要禁用多个issue时,用逗号把它们分隔开,比如:

1
tools:ignore="NewApi,StringFormatInvalid"

要在某个XML元素中对所有issue都禁用lint检查,可以使用all关键字,比如:

1
tools:ignore="all"

自定义lint

为什么需要自定义lint

由于每个项目自身的需求,Android Lint默认的检查项目可能不能满足我们的需求。
比如我们自己写了一个下拉刷新的库项目,可以让用户直接在xml布局文件中去使用它,但是我们希望用户必须在这个xml元素中定义一个pullmode属性,否则组件无法正常运行,我们希望lint能够对此进行检查,并在用户忘记添加此属性时给出明确的错误提示。再比如,我们的项目中使用了自己封装的日志库,能够方便的在release版本中关闭日志输出来防止app的效率下降,该日志库还能够把日志输出到指定的文件中方便事后分析,这时有一位新成员加入了我们的开发,他可能还是习惯性的用android.util.Log来打印日志,我们希望能够检测到本项目中所有使用了android.util.Log的代码,并发出警告。
要满足这些自定义需求,我们就需要通过Android Lint的扩展机制自己定制lint规则。

自定义lint如何使用

自定义lint是一个纯java项目,以jar的形式输出。有了包含lint规则的jar后,有两种使用方案:

  • 方案一:把此jar拷贝到 ~/.android/lint/ 目录中(文件名任意)。此时,这些lint规则针对所有项目生效。
  • 方案二:继续创建一个Android library项目,用来输出包含lint.jar的aar;然后,让目标项目依赖此aar即可使自定义lint规则生效。

由于方案一是全局生效的策略,无法单独针对目标项目,用处不大。在工程实践中,我们主要使用方案二。

AAR是Android Library的一种新的二进制分发格式,它把资源也一起打包,这样一来图片和布局资源文件也能够被同时分发。AAR格式文件能够包含一个可选的lint.jar文件,如果一个app依赖了一个包含lint.jar的aar文件,那么这个lint.jar中的规则就会在app的lint任务中被用来做lint检查。

自定义lint实现原理

自定义lint规则是以jar形式存在的,主要通过继承两种类来实现扩展lint功能:
①继承IssueRegistry:这是自定义Lint规则的主类或者叫注册类,有且仅有一个,用来注册这个自定义Lint项目中有哪些自定义的issue(issue就是需要lint检查出来并报告给用户的各种问题)需要被检测。
②继承Detector并选择Detector中合适的XXXScanner接口来实现:在这里根据自身业务需求,实现各种自定义探测器(Detector),并定义各种issue,根据自身需求的不同这样的类可以有一个或多个。

事实上,Android系统默认的lint检查功能是通过BuiltinIssueRegistry类来定义的,在这个类的源码中可以看到定义的各种issue、detector,如图:

com.android.tools.lint.detector.api.Detector提供了7种XXXScanner接口,根据自身需要选择合适的接口去实现,下面把这7个接口的信息列出:

1、JavaScanner
功能:Specialized interface for detectors that scan Java source file parse trees

2、ClassScanner
功能:Specialized interface for detectors that scan Java class files

3、BinaryResourceScanner
功能:Specialized interface for detectors that scan binary resource files

4、ResourceFolderScanner
功能:Specialized interface for detectors that scan resource folders (the folder directory itself, not the individual files within it)

5、XmlScanner
功能:Specialized interface for detectors that scan XML files

6、GradleScanner
功能:Specialized interface for detectors that scan Gradle files

7、OtherFileScanner
功能:Specialized interface for detectors that scan other files

实现自定义Lint规则的过程,实际上就是实现detector的过程,每个detector能够定义1个或多个不同类型的issue。也就是说,一个detector能够检测多种issue,这些issue在逻辑上是有关联的,但这些issue可以拥有不同的严重程度、描述等,并能够独立地被抑制(suppress,即禁用对该issue的检查)。

自定义lint实战

下面简单演示一下开发一个自定义Lint规则的完整流程。

【1】在Android Studio中,打开或新建一个工程,然后点击【File -> New -> New Module】,在弹出窗口中选择新建一个Java Library,如图:

我们这里把Java Library命名为ljflintrules。

【2】自定义lint规则需要继承一些特定的类,所以需要在ljflintrules的build.gradle中添加依赖:

1
2
compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'

【3】在ljflintrules中新建一个LoggerUsageDetector类,用来检测用户代码中是否使用了android.util.Log类,如果有,就报告一个issue,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class LoggerUsageDetector extends Detector
implements Detector.ClassScanner {

public static final Issue ISSUE = Issue.create("LogUtilsNotUsed",
"You must use our `LogUtils`",
"Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.",
Category.MESSAGES,
9,
Severity.ERROR,
new Implementation(LoggerUsageDetector.class,
Scope.CLASS_FILE_SCOPE));

@Override
public List<String> getApplicableCallNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}

@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}

@Override
public void checkCall(@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode call)
{

String owner = call.owner;
if (owner.startsWith("android/util/Log")) {
context.report(ISSUE,
method,
call,
context.getLocation(call),
"You must use our `LogUtils`");
}
}
}

这段代码中,我们定义了一个ISSUE,定义时传入的6个参数意义如下:

  • LogUtilsNotUseds: 我们这条lint规则的id,这个id必须是独一无二的。
  • You must use our 'LogUtils':对这条lint规则的简短描述。
  • Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.:对这条lint规则更详细的解释。
  • Category.MESSAGES:类别。
  • 9:优先级,必须在1到10之间。
  • Severity.ERROR:严重程度。其他可用的严重程度还有FATAL、WARNING、INFORMATIONAL、IGNORE。
  • Implementation:这是连接Detector与Scope的桥梁,其中Detector的功能是寻找issue,而scope定义了在什么范围内查找issue。在我们的例子中,我们需要在字节码级别分析用户有没有使用android.util.Log

这个类中针对字节码中的android/util/Log进行了检查,并在发现时报告LogUtilsNotUsed这个issue。你也可以在这个类中定义多个issue,然后在代码逻辑中(比如checkCall方法中)针对不同的情况,抛出不同的issue。也就是说,一个XXXDetector是可以报告多种issue的。
如果需要检测更多问题,你也可以定义更多的XXXDetector类。XXXDetector类可以有多个。

【4】在ljflintrules中新建一个MyIssueRegistry类,它继承自IssueRegistry。这个类用来注册我们自己定义了哪些issue,这样lint在检查代码时才知道要针对哪些issue进行检查。代码如下:

1
2
3
4
5
6
7
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
System.out.println("!!!!!!!!!!!!! ljf MyIssueRegistry lint rules works");
return Arrays.asList(LoggerUsageDetector.ISSUE);
}
}

这个类中只有一个方法,就是返回一个List,其中包含了我们自定义的所有issue。
这里我们为了能够在控制台中清楚的看到我们自定义的lint规则是否被调用了,所以打印了一行提示信息。

【5】对于自定义lint生成的jar,我们必须在它的清单文件中指明它的主类。这里我们通过配置ljflintrules的build.gradle文件来完成这项工作:

1
2
3
4
5
jar {
manifest {
attributes('Lint-Registry': 'com.ljf.lintrules.MyIssueRegistry')
}
}

现在,你可以在控制台中通过命令./gradlew ljflintrules:assemble来执行编译任务,就可以输出我们需要的jar文件了。你可以在ljflintrules工程目录的build/libs/下找到ljflintrules.jar。

如果你想验证这个jar文件是不是真的有效,可以把它拷贝到~/.android/lint/目录下,然后在终端中输入lint --show LogUtilsNotUsed看看有没有输出我们定义的issue信息,有则表明自定义lint成功,如图:

测试完后记得把它从~/.android/lint/中删除。

【6】由于我们要把上一步生成的jar文件包含到一个aar中便于用户使用,所以我们还要在ljflintrules的build.gradle文件中添加以下信息:

1
2
3
4
5
6
7
8
9
configurations {
lintJarOutput
}

dependencies {
lintJarOutput files(jar)
}

defaultTasks 'assemble'

经过以上所有步骤,现在ljflintrules的build.gradle文件看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apply plugin: 'java'

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'
}

jar {
manifest {
attributes('Lint-Registry': 'com.ljf.lintrules.MyIssueRegistry')
}
}

configurations {
lintJarOutput
}

dependencies {
lintJarOutput files(jar)
}

defaultTasks 'assemble'

【7】新建一个Android Library项目,命名为ljflintrule_aar,用来输出aar,步骤如下:

在ljflintrule_aar的build.gradle的根节点加入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* rules for including "lint.jar" in aar
*/

configurations {
lintJarImport
}

dependencies {
lintJarImport project(path: ":ljflintrules", configuration: "lintJarOutput")
}

task copyLintJar(type: Copy) {
from (configurations.lintJarImport) {
rename {
String fileName ->
'lint.jar'
}
}
into 'build/intermediates/lint/'
}

project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
}

如果这时再编译项目,就会在ljflintrule_aar的输出目录中得到一个包含lint.jar的aar文件,这里的lint.jar就是我们在第5步中生成的ljflintrules.jar,只是换了个名字。

【8】在用户app中使用我们的自定义lint。
在用户自己的应用程序module中(我们这里就使用app module),打开app的build.gradle文件,在dependencies中加入以下依赖:

1
compile project(':ljflintrule_aar')

这里我们在app的MainActivity中使用了android自带的Log功能:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Log.d("tag", "dasfadsf");
}
}

在终端中,我们执行./gradlew lint来执行lint任务,可以在终端中看到以下输出:

输出中指出发现了1个error和2个warning,并给出了详细报告的地址。

我们在浏览器中打开html格式的详细报告,如下图所示:

以上8个步骤完整演示了如何自定义lint并使用它。

小结

本文对于自定义Lint规则的介绍主要是集中在总体开发流程,给出了一个简单的实例。在实际开发过程中,我们比较常见的需求是针对xml布局文件、java源代码等内容进行某些检查,受Lint开发API的限制需要用到AST的相关知识,以及lombok.ast开源库。由于lombok.ast开源库几乎无文档可用,所以还是需要花一定时间来阅读这个库的源码,并熟悉SDK自带的Lint源码如何使用这个库。如果你对自定义Lint感兴趣,可以关注下一篇文章的相关介绍。

文章目录
  1. 1. 什么是Android Lint
  2. 2. Android Lint检查哪些内容
  3. 3. 配置Android Lint
    1. 3.1. 在Android Studio中配置Lint
    2. 3.2. 配置Lint文件
    3. 3.3. 在Java源文件或XML源文件中配置lint检查
      1. 3.3.1. 在Java中配置lint检查
      2. 3.3.2. 在XML中配置lint检查
  4. 4. 自定义lint
    1. 4.1. 为什么需要自定义lint
    2. 4.2. 自定义lint如何使用
    3. 4.3. 自定义lint实现原理
  5. 5. 自定义lint实战
  6. 6. 小结